﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Reflection;

using King.Extensions;
using King.Reflection;
using King.Infoset;
using King.Infoset.Reflection;
using King.Infoset.Schema;
using King.Infoset.Entity;

namespace King.Client {

    public sealed class ClientRequest {

        #region Private Data Members
        private XsOperation m_operation;
        private object m_entity;
        private IEnumerable<object> m_arguments;
        #endregion

        #region Constructor
        internal ClientRequest(XsOperation operation, object entity, IEnumerable<object> arguments) {
            m_operation = operation;
            m_entity = entity;
            m_arguments = arguments;
        }
        #endregion

        #region Public Members
        public XsOperation Operation { get { return m_operation; } }
        public object Entity { get { return m_entity; } }
        public IEnumerable<object> Arguments() { return m_arguments; }
        #endregion
    }
    public abstract class ClientSession {

        #region Definitions
        private class SessionCache {

            #region Public Members
            public static ClientSession GetSession(object entity) {
                return s_entityToSession.GetValueOrDefault(entity);
            }
            #endregion

            #region Internal Members
            internal static void SetSession(object entity, ClientSession session) {
                s_entityToSession[entity] = session;
            }
            #endregion

            #region Constructor
            static SessionCache() {
                s_entityToSession = new Dictionary<object, ClientSession>();
            }
            #endregion

            #region Static Private Data Members
            private static Dictionary<object, ClientSession> s_entityToSession;
            #endregion
        }
        private class EntityCache {

            #region Definitions
            internal struct Key {

                #region Constructor
                internal Key(Type type, object id) {
                    Type = type;
                    Id = id;
                }
                #endregion

                #region Public Members
                public Type Type;
                public object Id;
                #endregion

                #region Object Overrides
                public override int GetHashCode() {
                    return base.GetHashCode();
                }
                public override bool Equals(object obj) {
                    if (!(obj is Key))
                        return false;

                    var rhs = (Key)obj;
                    if (Type != rhs.Type)
                        return false;

                    return Id.Equals(rhs.Id);
                }
                #endregion
            }
            internal class Pair {

                #region Constructor
                internal Pair(object client) {
                    Client = client;
                    Server = null;
                }
                internal Pair(object client, object server) {
                    Server = server;
                    Client = client;
                }
                #endregion

                #region Public Members
                public object Server;
                public object Client;
                #endregion
            }
            #endregion

            #region Private Data Members
            private ClientSession m_session;
            private Dictionary<Key, Pair> m_cache;
            #endregion

            #region Constructor
            internal EntityCache(ClientSession session) {
                m_session = session;
                m_cache = new Dictionary<Key, Pair>();
            }
            #endregion

            #region Private Member
            private Pair GetPair(Type type, long id) {
                Pair cached;
                var key = new Key(type, id);
                if (!m_cache.TryGetValue(key, out cached)) {
                    var contract = XsLoader.GetContract(type);

                    // activate entity with default values supplied
                    var client = contract.DeserializedType.Activate();

                    // deserialize the id into the entity
                    contract.GetEntityMember().SetValue(client, id);

                    // save a copy of the entity for change tracking
                    var server = VisitorNode.Copy(client);

                    cached = SetPair(key, client, server);
                }
                return cached;
            }
            private Pair SetPair(Key key, object client, object server) {
                Pair cached;

                // cache the entity
                m_cache[key] = cached = new Pair(client, server);

                // attach the entity to this session
                SessionCache.SetSession(client, m_session);

                return cached;
            }
            #endregion

            #region Internal Members
            internal object Get(Type type, long id) {
                object server;
                return Get(type, id, out server);
            }
            internal object Get(Type type, long id, out object server) {
                var pair = GetPair(type, id);
                server = pair.Server;
                return pair.Client;
            }
            internal void Set(object client) {
                var contract = XsLoader.GetContract(client);
                var id = contract.GetId(client);
                var type = client.GetType();
                var key = new Key(type, id);

                var server = VisitorNode.Copy(client);
                SetPair(key, client, server);
            }
            internal IEnumerable<Pair> Pairs() {
                return m_cache.Values;
            }
            #endregion
        }
        #endregion

        #region Static Public Members
        public static ClientSession GetSessionOrNull(object entity) {
            var result = SessionCache.GetSession(entity);
            return result;
        }
        public static ClientSession GetSession(object entity) {
            var result = GetSessionOrNull(entity);
            if (result == null)
                throw new ArgumentException("Entity has no associated session.");
            return result;
        }
        #endregion

        #region Private Data Members
        private EntityCache m_cache;
        private XsOperation m_updateOperation;
        #endregion

        #region Constructor
        public ClientSession(XsOperation updateOperation) {
            m_cache = new EntityCache(this);
            m_updateOperation = updateOperation;
        }
        #endregion

        #region Private Members
        private object Merge(object client, object server) {
            // everything the server returned to should be copied into the client 
            // as the server should only return values that changed during the update
            client = VisitorNode.LeftCopy(server, client, client,
                (l, r) => true,
                (l, r) => !l.Equals(r),
                (l, r) => l);

            // #merge - update the server object to be a copy of the client 
            m_cache.Set(client);

            return client;
        }
        private object MergeResult(ClientRequest request, Dictionary<int, object> entitiesByIndex, VisitorNode node) {
            XsOperation operation = request.Operation;

            if (node.Value == null)
                return null;

            // the entity is the only thing that never undergoes transformation
            var contract = node.Contract;
            if (!contract.IsEntity())
                return node.ShallowCopyValue();

            // server returned an entity
            if (operation.IsCreate() || operation.IsUpdate() || operation.IsRefresh()) {
                return Merge(entitiesByIndex[node.Index], node.Value);

                // server returned an entityRef
            } else {
                var id = contract.GetId(node.Value);
                return m_cache.Get(contract.DeserializedType, id);
            }
        }
        private Dictionary<int, object> GetRequestEntitesByIndex(ClientRequest request) {
            var entitiesByIndex = new Dictionary<int, object>();
            var operation = request.Operation;
            var index = 0;

            // create
            if (operation.IsCreate()) {
                var entities = request.Arguments();
                var entity = entities.Single();
                entitiesByIndex[index] = entity;

                // update
            } else if (operation.IsUpdate()) {
                var entity = request.Entity;
                entitiesByIndex[index] = entity;

                // refresh
            } else if (operation.IsRefresh()) {

                // batch refresh
                if (operation.IsStatic) {
                    var entities = (IEnumerable)request.Arguments().Single();
                    entitiesByIndex = entities.Cast<object>().ToDictionary(o => index++);

                    // incremental refresh
                } else {
                    var entity = request.Entity;
                    entitiesByIndex[index] = entity;
                }
            }

            return entitiesByIndex;
        }
        private bool ShouldCopyToUpdate(VisitorNode client, VisitorNode server) {

            if (!client.HasValue)
                return false;

            // if the client and server are the same then return null
            if (client.Equals(server))
                return false;

            // if the client has a non-null value then return that
            if (client.HasValue)
                return true;

            // must have a member to get a contract to get a default value
            var member = client.Member;
            if (member == null)
                throw new ArgumentException();

            // must have a default value
            if (member.EmptyValue == null)
                throw new ArgumentException();

            return true;
        }
        private VisitorNode CopyToUpdate(VisitorNode client, VisitorNode server) {
            if (client.HasValue)
                return client;

            // deserialize the default value
            var member = client.Member;
            var deserializedEmptyValue = member.Contract.Deserialize(member.EmptyValue);
            return client.ShallowCopy(o => deserializedEmptyValue);
        }
        private bool TraverseChildrenDiff(VisitorNode client, VisitorNode server, object parent, List<ClientRequest> subUpdates) {

            var equals = client.Equals(server);
            if (equals)
                return false;

            // traverse children if the values are different and not handling the entity case
            var member = client.Member;
            if (member == null)
                return !equals;

            var createOnAssociate = member.GetAttribute<CreateOnAssociateAttribute>();
            if (!member.Contract.IsEntityRef() || createOnAssociate == null)
                return !equals;

            // bind to the createOnAssociate operation & build a ClientRequest to create and associate the entity
            var operation = member.DeclaringContract.Operations().Single(o => o.Name == createOnAssociate.CreateOperationName);
            var clientRequest = new ClientRequest(operation, parent, new[] { client.DeserializedValue });
            subUpdates.Add(clientRequest);

            return false;
        }
        private IEnumerable<ClientRequest> FindUpdates(XsOperation operation, object client, object server) {

            var updates = new List<ClientRequest>();
            var update = VisitorNode.LeftCopy(client, server,
                // detect CreateOnAssociateAttribute here and remember to cache parent
                (l, r) => TraverseChildrenDiff(l, r, client, updates),
                (l, r) => ShouldCopyToUpdate(l, r),
                (l, r) => CopyToUpdate(l, r));

            if (update != null) {
                // copy the version if differences are found
                var contract = XsLoader.GetContract(client);
                contract.SetVersion(update, contract.GetVersion(client));

                updates.Add(new ClientRequest(operation, client, new[] { update }));
            }

            return updates;
        }
        #endregion

        #region Public Members
        public object Get(Type type, long id) {
            return m_cache.Get(type, id);
        }
        public T Get<T>(long id) {
            return (T)Get(typeof(T), id);
        }

        public object TransformResult(ClientRequest request, object response) {

            var entitiesById = GetRequestEntitesByIndex(request);

            var result = VisitorNode.Transform(
                response,
                // MergeResult will handle any entities
                o => o.HasValue && !o.Contract.IsEntity(),
                o => true,
                node => MergeResult(request, entitiesById, node));

            // casees not handled by code:#merge
            var operation = request.Operation;
            if (operation.IsUpdate()) {
                if (response == null)
                    m_cache.Set(request.Entity);

            } else if (operation.IsCreate()) {
                // NetworkBuilder.Network case
                if (request.Entity != null)
                    m_cache.Set(request.Entity);

                if (response == null)
                    m_cache.Set(result);
            }

            return result;
        }
        public abstract IEnumerable<object> Invoke(IEnumerable<ClientRequest> requests);
        public IEnumerable<object> Invoke(params ClientRequest[] requests) {
            var result = Invoke((IEnumerable<ClientRequest>)requests);
            return result;
        }

        public object Invoke(XsOperation operation, object entity, IEnumerable<object> arguments) {
            var clientRequest = new ClientRequest(operation, entity, arguments);
            return Invoke(clientRequest).Single();
        }
        public object Invoke(XsOperation operation, object entity, params object[] arguments) {
            return Invoke(operation, entity, (IEnumerable<object>)arguments);
        }

        public T Invoke<T>(MethodBase methodBase, object entity, IEnumerable<object> arguments) {
            var contract = XsLoader.GetContract(methodBase.DeclaringType);
            var operation = contract.GetOperation(methodBase);
            return (T)Invoke(operation, this, arguments);
        }
        public T Invoke<T>(MethodBase methodBase, object entity, params object[] arguments) {
            return Invoke<T>(methodBase, entity, (IEnumerable<object>)arguments);
        }

        public void SubmitChanges() {
            var updates = m_cache.Pairs().SelectMany(o => FindUpdates(m_updateOperation, o.Client, o.Server));
            if (updates.None())
                return;

            Invoke(updates);
        }
        #endregion
    }
    public class ClientSet<T> : IEnumerable<T> {

        #region Private Data Members
        private object m_entity;
        private string m_name;

        private XsContract m_contract;
        private XsOperation m_getOperation;
        private XsOperation m_setOperation;
        private XsOperation m_addOperation;
        private XsOperation m_createOperation;
        private XsOperation m_removeOperation;
        #endregion

        #region Constructor
        public ClientSet(object entity, string name) {
            m_name = name;
            m_entity = entity;
            m_contract = XsLoader.GetContract(entity);
        }
        #endregion

        #region Private Members
        private XsOperation ResolveOperation(string verb, string noun) {
            return m_contract.Operations(verb + noun).SingleOrDefault();
        }
        private object Invoke(XsOperation operation, IEnumerable arguments) {
            if (operation == null)
                throw new NotSupportedException();
            return Session.Invoke(operation, m_entity, arguments.ToEnumerableOfObject());
        }
        #endregion

        #region Public Members
        public ClientSession Session {
            get { return ClientSession.GetSession(m_entity); }
        }
        public XsOperation GetOperation {
            get {
                if (m_getOperation == null)
                    m_getOperation = ResolveOperation("Get", m_name);
                return m_getOperation;
            }
        }
        public XsOperation SetOperation {
            get {
                if (m_setOperation == null)
                    m_setOperation = ResolveOperation("Set", m_name);
                return m_setOperation;
            }
        }
        public XsOperation CreateOperation {
            get {
                if (m_addOperation == null && m_createOperation == null)
                    m_createOperation = ResolveOperation("Create", m_name.TrimEnd('s'));
                return m_createOperation;
            }
        }
        public XsOperation AddOperation {
            get {
                if (m_addOperation == null && m_createOperation == null)
                    m_addOperation = ResolveOperation("Add", m_name);
                return m_addOperation;
            }
        }
        public XsOperation RemoveOperation {
            get {
                if (m_removeOperation == null)
                    m_removeOperation = ResolveOperation("Remove", m_name);
                return m_removeOperation;
            }
        }
        public XsContract Contract {
            get { return m_contract; }
        }
        public object Entity {
            get { return m_entity; }
        }
        #endregion

        #region Public Members (Add, Set, Remove, Refresh, Release)
        public T Add(T element) {
            Add(new[] { element });
            return element;
        }

        public void Set(params T[] elements) {
            Set((IEnumerable<T>)elements);
        }
        public void Add(params T[] elements) {
            Add((IEnumerable<T>)elements);
        }
        public void Remove(params T[] elements) {
            Remove((IEnumerable<T>)elements);
        }

        public void Set(IEnumerable<T> elements) {
            Invoke(SetOperation, new[] { elements });
        }
        public void Add(IEnumerable<T> elements) {
            if (AddOperation != null)
                Invoke(AddOperation, new object[] { elements });

            else {
                foreach (var o in elements)
                    Invoke(CreateOperation, new[] { o });
            }
        }
        public void Remove(IEnumerable<T> elements) {
            Invoke(RemoveOperation, new[] { elements });
        }
        #endregion

        #region IEnumerable<T> Members
        public IEnumerator<T> GetEnumerator() {
            foreach (var element in ((IEnumerable)Invoke(GetOperation, Enumerable.Empty<T>())).CastOrEmpty<T>())
                yield return element;
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }
        #endregion
    }
}
